/**
 * Jaxson ajax
 *
 * Copyright Remedy Interactive, licensed under Apache version 2 license:
 *
 *  http://www.apache.org/licenses/LICENSE-2.0.html
 *
 * This notice must remain in all java source files.
 */
package org.jaxson.struts;


import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.struts2.interceptor.ServletRequestAware;
import org.apache.struts2.interceptor.ServletResponseAware;
import org.jaxson.struts.charts.AbstractChart;
import org.jaxson.struts.charts.ImageMapAction;
import org.jaxson.struts.charts.ImageMapModel;
import org.jaxson.struts.encoder.EncodingException;
import org.jaxson.struts.encoder.JpegEncoder;
import org.jaxson.struts.encoder.PdfEncoder;
import org.jaxson.struts.encoder.ResponseContentType;
import org.jaxson.struts.encoder.ResponseEncoder;
import org.jaxson.struts.json.JsonParameterAnnotations;
import org.jaxson.struts.json.JsonParameterAnnotations.JsonParameterDescriptor;
import org.jaxson.struts.postprocessor.PostRequestProcessor;
import org.jaxson.util.reflection.ReflectionUtil;
import org.jaxson.validation.AbstractValidator;
import org.jaxson.validation.SessionAttributePresent;
import org.jaxson.validation.Validation;
import org.jaxson.validation.ValidationErrorMessage;
import org.jaxson.validation.ValidationErrors;
import org.jaxson.validation.Validator;
import org.jaxson.validation.Validator.Scope;

import com.lowagie.text.Document;
import com.opensymphony.xwork2.ActionSupport;
import com.opensymphony.xwork2.ModelDriven;

import flexjson.JSONDeserializer;

/**
 * Base action class for all struts actions.
 * 
 * @author Joe Maisel
 */
public abstract class BaseAction extends ActionSupport implements ServletRequestAware, ServletResponseAware, ModelDriven<Serializable>
{
	private static final Log lager = LogFactory.getLog( BaseAction.class.getName() );

	private static final long serialVersionUID = 3792656766797268689L;

	// protected members, available in subclasses.
	protected ResponseEncoder encoder;
	protected HttpServletRequest request;
	protected HttpServletResponse response;
	protected HttpSession session;
	protected Serializable model;

	// private members
	private boolean cacheResponse;
	private AbstractChart chart;
	private PostRequestProcessor postRequestProcessor;
	private ResponseContentType rtype = ResponseContentType.html;
	private List<ResponseContentType> enabledResponses = new ArrayList<ResponseContentType>();
	private static final Map<Class<? extends BaseAction>, List<AbstractValidator>> validators = new HashMap<Class<? extends BaseAction>, List<AbstractValidator>>();
	private static final Map<Class<? extends BaseAction>, SessionAttributePresent> requiredSessionAttrs = new HashMap<Class<? extends BaseAction>, SessionAttributePresent>();
	
	public BaseAction ()
	{
		enabledResponses.add( ResponseContentType.html );
	}

	/**
	 * Handle a request
	 */
	public abstract String handle();
	
	private void parseValidationAnnotations() 
	throws SecurityException, NoSuchMethodException, InstantiationException, IllegalAccessException{
		
		lager.debug( getClass().getName() + ".parseValidationAnnotations()" );
		Class<? extends BaseAction> cls = this.getClass();
		Method m = cls.getMethod("handle");
		Validation v = m.getAnnotation( Validation.class );
		
		if( v == null ){
			lager.debug( getClass() + " no validators configured" );
			return;
		}
		
		Validator[] validators = v.validators();
		
		for( Validator validator : validators ){
			Class<? extends AbstractValidator> vcls = validator.validator();
			
			for( String prop : validator.properties() ){
				AbstractValidator av = vcls.newInstance();
				String[] args = validator.args();
				
				for( String arg : args ){
					arg = arg.trim();
					String name = arg.split("=")[0];
					String value = arg.split("=")[1];
					ReflectionUtil.set(av, name, value);
				}
				
				av.setProperty(prop);
				av.setScope(av.getScope());
				BaseAction.validators.get(this.getClass()).add(av);
			}
		}
	}
	
	public void doValidation(){
		List<AbstractValidator> validators = BaseAction.validators.get(this.getClass());
		ValidationErrors errors = new ValidationErrors();
		
		lager.debug( "validating class: " + this.getClass() + " validators: " + BaseAction.validators.get( this.getClass() ) );
		
		for( AbstractValidator av : validators ){
			Object ctx = av.getScope() == Scope.MODEL?getModel():this;
			Object value = ReflectionUtil.getProperty(ctx, av.getProperty());
			
			if( !av.isValid(ctx) ){
				ValidationErrorMessage msg = new ValidationErrorMessage();
				msg.setActualValue(value);
				msg.setSource(av);
				
				errors.add(av);
			}
		}
		
		if( errors.size() > 0 )
			setModel(errors);
	}
	
	/**
	 * Struts entry point. Calls <code>handle()</code> which should return a
	 * String specifying the page struts will forward to after execution
	 * completes.
	 */
	@SuppressWarnings("unchecked")
	public String execute()
	{
		if( !BaseAction.requiredSessionAttrs.containsKey(this.getClass())){
			SessionAttributePresent s = getClass().getAnnotation(SessionAttributePresent.class);
			if( s != null ){
				if( session.getAttribute(s.key()) == null ){
					response.setStatus(s.statusCode());
					return null;
				}
			}
		}
		
		if( !BaseAction.validators.containsKey(this.getClass()) ){
			try {
				BaseAction.validators.put( this.getClass(), new ArrayList<AbstractValidator>() );
				parseValidationAnnotations();
			}
			catch (SecurityException e) {
				e.printStackTrace();
			}
			catch (NoSuchMethodException e){
				e.printStackTrace();
			}
			catch (InstantiationException e){
				e.printStackTrace();
			}
			catch (IllegalAccessException e){
				e.printStackTrace();
			}
		}
		
		try{
			JsonParameterAnnotations.parse( getClass() );
			lager.debug( "parsed " + getClass() );
		}
		catch (SecurityException e){
			e.printStackTrace();
		} 
		catch (NoSuchMethodException e){
			e.printStackTrace();	
		}
		
		for( JsonParameterDescriptor p : JsonParameterAnnotations.get(getClass()) ){
			if( request.getParameter(p.parameter.name()) != null ){
				Object obj = new JSONDeserializer().use(null, p.type).deserialize(request.getParameter(p.parameter.name()));
				try {
					p.setter.invoke(this, obj);
				}
				catch (Exception e) {
					e.printStackTrace();
					throw new RuntimeException(e);
				}
			}
		}
		
		doValidation();
		String result = null;
		
		if( !(model instanceof ValidationErrors) ){
			result = handle();
		}
		
		// set the content type header
		this.response.setContentType( rtype.getMimeType() );
		
		if( !this.enabledResponses.contains( this.rtype ) )
			throw new UnsupportedOperationException( "The requested response type " + this.rtype + " isn't enabled." );

		// set the content type header
		this.response.setContentType( rtype.getMimeType() );

		// disable caching
		if( !this.cacheResponse ){
			this.response.addHeader( "Cache-Control", "post-check=0, pre-check=0" );
			this.response.setHeader( "Expires", "Sat, 6 May 1995 12:00:00 GMT" );
			this.response.setHeader( "Cache-Control", "no-store, no-cache, must-revalidate" );
			this.response.setHeader( "Pragma", "no-cache" );
		}

		if( rtype != ResponseContentType.html ){
			encode( model );
			result = null;
		}

		PostRequestProcessor prp = this.getPostRequestProcessor();

		if( prp != null ){
			prp.setRequest( request );
			prp.setResponse( response );
			// TODO: refactor to support a list of PostRequestProcessors
			// PostRequestProcessorThread thread = new
			// PostRequestProcessorThread(session, prp);

			// thread.start();
		}

		return rtype != ResponseContentType.html?null:result;
	}

	/**
	 * Enable a specific type of response. HTML is supported by default.
	 */
	public void enableResponseType( ResponseContentType type )
	{
		if( !enabledResponses.contains( type ) )
			enabledResponses.add( type );
	}

	/**
	 * Disable a specific type of response. HTML is supported by default.
	 */
	public void disableResponseType( ResponseContentType type )
	{
		if( enabledResponses.contains( type ) )
			enabledResponses.remove( type );
	}

	/**
	 * @return the chart
	 */
	public AbstractChart getChart()
	{
		return chart;
	}

	/**
	 * @param chart the chart to set
	 */
	public void setChart( AbstractChart chart )
	{
		this.chart = chart;
	}

	/**
	 * For content types other than text/html, gets a new instance of the
	 * encoding class and invokes the encoding logic.
	 * 
	 * @param model
	 */

	protected void encode( Serializable model )
	{
		try
		{
			response.setContentType( rtype.getMimeType() );
			encoder.setOutputStream( response.getOutputStream() );

			if( encoder instanceof PdfEncoder )
			{
				((PdfEncoder) encoder).setDocument( getDocument() );
			}

			else if( encoder instanceof JpegEncoder )
			{
				((JpegEncoder) encoder).setChart( getChart() );
			}

			encoder.encode( model );

			if( encoder instanceof JpegEncoder )
			{
				String imap = ((JpegEncoder) encoder).getImageMap();

				ImageMapModel m = new ImageMapModel();
				m.setImageId( getChart().getFormat().getImageId() );
				m.setImageMapId( getChart().getFormat().getImageMapId() );
				m.setImageMap( imap );

				session.setAttribute( ImageMapAction.sessionKey( getChart().getFormat().getImageId() ), m );
			}
		}
		catch( IOException e )
		{
			e.printStackTrace();
		}
		catch( EncodingException e )
		{
			e.printStackTrace();
		}
	}

	/**
	 * Exists as extension point for pdf rendering. Subclasses wishing to
	 * provide PDF rendering capabilities should implement
	 * org.jaxson.struts.encoder.PdfAware.
	 * 
	 * @see org.jaxson.struts.encoder.PdfAware
	 */
	protected Document getDocument()
	{
		return null;
	}

	/**
	 * @return the postRequestProcessor
	 */
	public PostRequestProcessor getPostRequestProcessor()
	{
		return postRequestProcessor;
	}

	/**
	 * @param postRequestProcessor
	 *            the postRequestProcessor to set
	 */
	public void setPostRequestProcessor( PostRequestProcessor postRequestProcessor )
	{
		lager.debug( "setting Request processor " + postRequestProcessor );
		this.postRequestProcessor = postRequestProcessor;
	}

	/**
	 * Sets the content type http header in the response and the appropriate
	 * encoder for content types other than 'text/html'
	 */
	public void setResponseContentType( ResponseContentType rt )
	{
		this.rtype = rt;
		try
		{
			this.encoder = rtype.getEncoder();
		}
		catch( InstantiationException e )
		{
			e.printStackTrace();
		}
		catch( IllegalAccessException e )
		{
			e.printStackTrace();
		}
	}

	/**
	 * Returns true if the request is made with an XMLHttpRequest object. Checks
	 * the "X-Requested-With" header.
	 */
	public boolean isAjaxRequest()
	{
		String s = request.getHeader( "X-Requested-With" );
		return s != null && s.equals( "XMLHttpRequest" );
	}

	/**
	 * @see org.apache.struts2.interceptor.ServletResponseAware#setServletResponse(javax.servlet.http.HttpServletResponse)
	 */
	public void setServletResponse( HttpServletResponse response )
	{
		this.response = response;
	}

	/**
	 * @see org.apache.struts2.interceptor.ServletRequestAware#setServletRequest(javax.servlet.http.HttpServletRequest)
	 */
	public void setServletRequest( HttpServletRequest request )
	{
		this.request = request;
		this.session = request.getSession();
	}

	/**
	 * @param model
	 *            the model to set
	 */
	public void setModel( Serializable model )
	{
		this.model = model;
	}

	/**
	 * @see com.opensymphony.xwork2.ModelDriven#getModel()
	 */
	public Serializable getModel()
	{
		return this.model;
	}

	/**
	 * Sets the response content type if the parameter 'returnAs' is available
	 * in the request.
	 * 
	 * @param returnAs
	 *            the returnAs to set
	 */
	public void setReturnAs( String returnAs )
	{
		setResponseContentType( ResponseContentType.valueOf( returnAs ) );
	}
	
	/**
	 * @return the returnAs
	 */
	public String getReturnAs()
	{
		return rtype.name();
	}
	
	/**
	 * Get the ResponseContentType
	 */
	public ResponseContentType getResponseContentType()
	{
		return rtype;
	}
	
	/**
	 * @return the cacheResponse
	 */
	public boolean isCacheResponse()
	{
		return cacheResponse;
	}

	/**
	 * @param cacheResponse the cacheResponse to set
	 */
	public void setCacheResponse( boolean cacheResponse )
	{
		this.cacheResponse = cacheResponse;
	}
}
